package app_kvEcs;

import infrastructure.DataReader;
import infrastructure.HasherService;
import infrastructure.KVMessageImp;
import infrastructure.LoggingManager;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import clientCommunication.ClientSocketListener;
import clientCommunication.CommunicationFactory;
import clientCommunication.ICommunication;
import common.messages.KVMessage;
import common.messages.KVMessage.MessageType;
import common.messages.KVMessage.StatusType;

public class ECS implements IECS, ClientSocketListener{
	private Logger logger = LoggingManager.CreateEcsLogger(ECS.class);
	
	private HasherService hasherService;
	private List<ServerInfo> serversInfo;
	private Map<ServerInfo, Process> processes;
	private Map<ServerInfo, ICommunication> servers;
	private int amountOfServers;
	private int countOfServers;
	String [] hashMetadata;
	String script;
	String keyPort = "<port>";
	String keyHost = "<host>";
	/**
	 * Creates a new External Configuration Servive
	 * @param serversData
	 * @throws Exception In case file for script is not founded
	 */
	public ECS(List<String[]> serversData, String scriptFile) throws Exception {
		DataReader scriptReader = new DataReader(scriptFile, "");
		script = scriptReader.getDataByLines().get(0);
		
		this.processes = new HashMap<ServerInfo, Process>();
		this.serversInfo = new ArrayList();
		this.servers = new HashMap<ServerInfo, ICommunication>();
		this.amountOfServers = this.setServerInfo(serversData);
		this.countOfServers = 0;
		this.hasherService = new HasherService();
	}
	
	/**
	 * Set the data of the servers available
	 * @param serversData
	 * @return
	 */
	private int setServerInfo(List<String[]> serversData) {
		for (String [] metadata : serversData) {
			serversInfo.add(new ServerInfo(metadata[1], Integer.parseInt(metadata[2])));
		}
		
		int amount = this.serversInfo.size();
		logger.info(String.format("%s servers were identified", amount));
		return amount;
	}

	/**
	 * Init the servers with the amount of servers in count.
	 */
	public boolean initServer(int count) {
		if (this.isServerRunning()){
			logger.warn(String.format("Server is already running"));
			return false;
		}
		
		if (count > this.amountOfServers) {
			logger.warn(String.format("Server was not started because there is not enought amount of servers %s", countOfServers));
			return false;
		}
		
		this.countOfServers = count;
		
		for (int i =0 ; i < this.countOfServers; i++) {
			ServerInfo si = serversInfo.get(i);			
			this.addNode(si);
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				logger.error(e);
			}
		}
		
		return true;
	}
	
	/**
	 * Deliver the message to the client, whic means to print the message
	 * @param kvMessage
	 */
	private void deliverMessage(KVMessage kvMessage) {
		logger.info("Receiving message from server");
		String textMessage = kvMessage.getValue();
		System.out.println(textMessage);
	}
	
	/**
	 * Calculates the MD5 hash data
	 * @return
	 */
	private String [] calculateHashServersData() {
		logger.info("Calculating hash data");
		String [] returnValues = new String[2];
		returnValues[0] ="";
		returnValues[1] ="";
		List<ServerInfo> serversRunning = this.getServersRunning();
		if (serversRunning.size() > 0) {
			List<String> ranges = this.hasherService.getUpperBoundHashRangeSorted(serversRunning.size());
			int i = 0;
			for (ServerInfo si : serversRunning) {		
				returnValues[0] += ranges.get(i) + ";";
				returnValues[1] += si.toString() + ";";
				i++;			
			}
		}
		logger.info("Hash data calculated");
		return returnValues;
		
	}

	public void start(){
		this.hashMetadata = this.calculateHashServersData();
		this.start(this.hashMetadata);
	}
	
	private void start(String [] newhashMetadata){		
		KVMessageImp message = new KVMessageImp(MessageType.METADATA, newhashMetadata[1], newhashMetadata[0], StatusType.START );
		this.broadcastMessage(message);
	}

	/*public void start(ServerInfo serverInfo){
		logger.info("starting server " + serverInfo);
		String [] hashData = this.calculateHashServersData();
		KVMessageImp message = new KVMessageImp(MessageType.METADATA, hashData[1], hashData[0], StatusType.START);
		this.sendMessage(serverInfo, message);
	}*/
	
	public void stop() {
		KVMessageImp message = new KVMessageImp(MessageType.METADATA, StatusType.STOP);
		this.broadcastMessage(message);
	}
	
	public void shutDown(){
		System.out.println("Shutting down");
		//for (ICommunication server : servers.values()){
		//	server.disconnect();
		//}
		
		
		//for (Process p:processes.values()){
		//	p.destroy();
		//}
		String [] emptyData = new String [] { "", "" };
		for (ServerInfo si:this.serversInfo){
			if (si.getStatus() == ServerStatus.ADDED){
				this.removeNode(si, emptyData);
			}
			//if (si.getStatus() == ServerStatus.ADDED){
			//	si.setStatus(ServerStatus.REMOVED);
			//}
		}
	}
	
	private void shutDown(ServerInfo serverInfo) {
		
		ICommunication communication = servers.remove(serverInfo);
		communication.disconnect();
		Process p = processes.remove(serverInfo);
		p.destroy();	
		serverInfo.setStatus(ServerStatus.REMOVED);		
		System.out.println("Server " + serverInfo + " was shut down.");
		logger.info("Server " + serverInfo + " was shut down.");
	}
	
	public void addNode() throws Exception {
		ServerInfo si = this.getNextAvaiableServer();
		if (si == null)
			throw new Exception("There is no more servers availables");
		
		this.addNode(si);		
		this.start();
		//this.reallocateMetaData();
	}
	
	private void addNode(ServerInfo si) {
		// Launch each server by SSH call. For now, just launch it from process
		Process proc;
		String script = this.getScript(si);//"./script.sh"; //"java -jar ms3-server.jar";		 
		Runtime run = Runtime.getRuntime();
		
		try {
		   //proc = run.exec(script+ " " + si.getPort());
		   proc = run.exec(script);
		   processes.put(si, proc);
		   si.setStatus(ServerStatus.ADDED);
		} catch (IOException e) {
			logger.error("Error starting process ", e);
			si.setStatus(ServerStatus.ERROR);
			return;
		}
	
		try {
		   ICommunication com = CommunicationFactory.createCommunication(si.getPort() + "", si.getIpAddress(), true);
		   com.run();
		   servers.put(si, com);
		   this.deliverMessage(com.receiveMessage());
		} catch (Exception e) {
			logger.error("Error connecting to server ", e);
			si.setStatus(ServerStatus.ERROR);
		}
	}

	private String getScript(ServerInfo si) {
		StringBuilder scriptWithParameters = new StringBuilder(this.script);
		int index = scriptWithParameters.indexOf(this.keyHost);
		if (index != -1)
			scriptWithParameters = scriptWithParameters.replace(index, index + this.keyHost.length(), si.getIpAddress());
		index = scriptWithParameters.indexOf(this.keyPort);
		if (index != -1)
			scriptWithParameters = scriptWithParameters.replace(index, index + this.keyPort.length(), si.getPort()+"");
		logger.info(scriptWithParameters.toString());
		return scriptWithParameters.toString();
	}

	public void removeNode() {
		if (!this.isServerRunning()) {
			logger.info("No more servers to disconnect");
			return;
		}

		ServerInfo serverInfo = this.getRandomServer();		
		serverInfo.setStatus(ServerStatus.REMOVING);
		String [] newMetadata = this.calculateHashServersData();
		this.removeNode(serverInfo, newMetadata);
		this.hashMetadata = newMetadata;
		if (this.isServerRunning())
			this.start(this.hashMetadata);
		// DAta reallocation occurs in each server
		
		//if (!this.isServerRunning()) {
		//	logger.info("No servers to reallocate the data");
		//	return;
		//}
		
		//this.reallocateData(dataMessage);
		
		// Start again. so the hash is redistributed
		//this.start();
	}	
	
	private void removeNode(ServerInfo serverInfo, String[] newHashMetadata) {
		logger.info("Server to be removed " + serverInfo);
		//this.lockServer(serverInfo);
		KVMessageImp message = new KVMessageImp(MessageType.METADATA, newHashMetadata[1], newHashMetadata[0], StatusType.SHUTDOWN);
		this.sendMessage(serverInfo, message);
		logger.info("Receiving confirmation message....");
		KVMessage confirmationMessage = this.receiveMessage(serverInfo);
		logger.info("End receiving confirmation message " + confirmationMessage);
		this.shutDown(serverInfo);
	}


	/*private void reallocateMetaData() {
		// TODO Auto-generated method stub
		this.lockServers();		
		try {
			// Because of sending two messages, we should wait, otherwise we get an exception/
			Thread.sleep(500);
		} catch (InterruptedException e) {
			logger.error(e);
		}		
		this.start();
	}*/

	/*private void reallocateData(KVMessage dataMessage) {
		ServerInfo serverInfo = this.getRandomServer();
		logger.info("Server to reallocated data " + serverInfo);
		this.lockServer(serverInfo);
		KVMessageImp message = new KVMessageImp(MessageType.METADATA, dataMessage.getValue(), dataMessage.getKey(), StatusType.PUT_ALL);		
		this.sendMessage(serverInfo, message);
		try {
			// Because of sending two messages, we should wait, otherwise we get an exception/
			Thread.sleep(500);
		} catch (InterruptedException e) {
			logger.error(e);
		}
		
		logger.info("FINISHING reallocating data");
	}*/

	private ServerInfo getRandomServer() {
		List<ServerInfo> randomArray = new ArrayList();
		for (ServerInfo si:this.serversInfo){
			if (si.getStatus() == ServerStatus.ADDED)
				randomArray.add(si);
		}
		
		int randomIndex = (int)Math.round(Math.random()*(randomArray.size() - 1));
		logger.info(String.format("Amount of servers to pick %s. Index selected %s ", randomArray.size(), randomIndex));
		return randomArray.get(randomIndex);
	}


	/*
	private void lockServers() {
		for (ServerInfo serverInfo:this.serversInfo){
			if (serverInfo.getStatus() == ServerStatus.ADDED)
				this.lockServer(serverInfo);
		}
	}
	
	private void lockServer(ServerInfo serverInfo) {
		logger.info("locking server " + serverInfo);
		KVMessageImp message = new KVMessageImp(MessageType.METADATA, StatusType.SERVER_WRITE_LOCK);
		this.sendMessage(serverInfo, message);
	}*/
	
	private KVMessage receiveMessage(ServerInfo serverInfo) {
		logger.info("Receive message from " + serverInfo);
		KVMessage message = null;
		ICommunication communication = this.servers.get(serverInfo);
		try {
			message = communication.receiveMessage();
		} catch (Exception e) {
			logger.info(serverInfo);
			logger.error(e);
			e.printStackTrace();
		}
		return message;
	}

	private void unlockServer(ServerInfo serverInfo) {
		logger.info("unlocking server " + serverInfo);
		KVMessageImp message = new KVMessageImp(MessageType.METADATA, StatusType.START);
		this.sendMessage(serverInfo, message);
	}
	
	private void sendMessage(ServerInfo serverInfo, KVMessageImp message) {
		logger.info("Sending Message " + serverInfo + " " + message);
		ICommunication communication = this.servers.get(serverInfo);
		try {
			message.setFrom("ECS");
			message.setTo(serverInfo.toString());
			communication.sendMessage(message);
		} catch (Exception e) {
			e.printStackTrace();
			logger.info(message);
			logger.error(e);
		}
	}
	
	private void broadcastMessage(KVMessageImp message){
		for (ServerInfo serverInfo : serversInfo){
			if (serverInfo.getStatus() == ServerStatus.ADDED)
				this.sendMessage(serverInfo, message);
		}
	}
	
	private boolean isServerRunning() {
		for(ServerInfo si:this.serversInfo){
			if (si.getStatus() == ServerStatus.ADDED)
				return true;
		}
		
		return false;
	}
	
	private int getCountServersRunning() {
		int counter = 0;
		for(ServerInfo si:this.serversInfo){
			if (si.getStatus() == ServerStatus.ADDED)
				counter++;
		}
		
		return counter;
	}
	
	private List<ServerInfo> getServersRunning() {
		List<ServerInfo> serversRunning = new ArrayList();
		for(ServerInfo si:this.serversInfo){
			if (si.getStatus() == ServerStatus.ADDED)
				serversRunning.add(si);
		}
		
		return serversRunning;
	}
	
	private ServerInfo getNextAvaiableServer() {
		for(ServerInfo si:this.serversInfo){
			if (si.getStatus() == ServerStatus.NONE)
				return si;
		}
		
		for(ServerInfo si:this.serversInfo){
			if (si.getStatus() == ServerStatus.REMOVED)
				return si;
		}
		
		return null;
	}
	
	@Override
	public synchronized void handleNewMessage(KVMessage kvMessage) {		
		// SYNC COMMUNICATION DO NOT USE THESE METHODS
		//logger.info("handleNewMessage Receiving message from server");
		//String textMessage = kvMessage.getValue();
		//System.out.println(textMessage);
	}

	@Override
	public void handleStatus(SocketStatus status) {
		// SYNC COMMUNICATION DO NOT USE THESE METHODS
		// TODO Auto-generated method stub
		// Remove process and reallocate data
		//System.out.println("Status " + status.toString());
		//logger.info("One server was disconnected " + status);
	}
}
